package per.sunmes.lesrb.data;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import com.badlogic.gdx.utils.Array;

import per.sunmes.lesrb.apis.Logger;
import per.sunmes.lesrb.apis.SaveDataOperation;
import per.sunmes.lesrb.util.GM;

@SuppressWarnings({ "rawtypes", "unchecked" })
public class DefaultSaveDataOperation implements SaveDataOperation {
	private static final String TAG = "[DefaultSaveDataOperation]";
	public static final int TYPE_BOOL = 0x11//
			, TYPE_BYTE = 0x12//
			, TYPE_SHORT = 0x13//
			, TYPE_INT = 0x14//
			, TYPE_LONG = 0x15//
			, TYPE_FLOAT = 0x20//
			, TYPE_DOUBLE = 0x21//

			, TYPE_STRING = 0x30//
			, TYPE_MASKINT = 0x31//

			, TYPE_ARRAY = 0x44//
			, TYPE_MAP = 0x55//

			, TYPE_IS_MUSIC_ON = 0x61//
			, TYPE_IS_SOUND_ON = 0x62//

			, TYPE_KEY_INT = 0x90//
			, TYPE_KEY_STRING = 0x91//

			, TYPE_STACK_RETURN = 0x99//

			, FILE_SIGN = 0x899822//
	;

	public static boolean isLogOut = false;
	int stackLevel;

	@Override
	public SaveData load(InputStream in) {
		SaveData data = new SaveData();

		DataInputStream input = null;
		do {
			try {
				input = new DataInputStream(in);
				debug("StartReading.");
				stackLevel = 0;

				int fileSign = input.readInt();
				if (fileSign != FILE_SIGN) {
					log("Not saveFile. FileSign not valid!");
					break;
				}
				debug("Read FileSign OK.");
				////////////
				while (inputReadSet(input, data) != TYPE_STACK_RETURN) {
				}
				debug("Read FileEnd StackReturn! Read done!");

			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				if (input != null) {
					try {
						input.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
		} while (false);

		return data;
	}

	private int inputReadSet(DataInputStream input, SaveData data) throws IOException {
		int type = input.readInt();
		switch (type) {
		case TYPE_IS_MUSIC_ON:
			debug(">ReadType: MusicOn");
			data.isMusicOn = input.readBoolean();
			debug("    " + data.isMusicOn);
			break;
		case TYPE_IS_SOUND_ON:
			debug(">ReadType: SoundOn");
			data.isSoundOn = input.readBoolean();
			debug("    " + data.isSoundOn);
			break;
		case TYPE_KEY_INT:
			debug(">ReadType: KEY Int");
			readField(null, null, input.readInt(), input, data);
			break;
		case TYPE_KEY_STRING:
			debug(">ReadType: KEY String");
			readField(null, null, input.readUTF(), input, data);
			break;
		}
		return type;
	}

	private int readField(Array array, Map map, Object key, DataInputStream input, SaveData data) throws IOException {
		// debug(StringUtil.format(" >Array[%s]Map[%s]Key[%s]", array, map,
		// key));
		if (key != null) {
			debugField(" KEY ", key);
		}
		int type = input.readInt();
		switch (type) {
		case TYPE_BOOL:
			inputReadBoolean(array, map, key, input.readBoolean(), data);
			break;
		case TYPE_BYTE:
			inputReadByte(array, map, key, input.readByte(), data);
			break;
		case TYPE_SHORT:
			inputReadShort(array, map, key, input.readShort(), data);
			break;
		case TYPE_INT:
			inputReadInt(array, map, key, input.readInt(), data);
			break;
		case TYPE_LONG:
			inputReadLong(array, map, key, input.readLong(), data);
			break;
		case TYPE_FLOAT:
			inputReadFloat(array, map, key, input.readFloat(), data);
			break;
		case TYPE_DOUBLE:
			inputReadDouble(array, map, key, input.readDouble(), data);
			break;
		case TYPE_STRING:
			inputReadString(array, map, key, input.readUTF(), data);
			break;
		case TYPE_MASKINT:
			inputReadMaskInt(array, map, key, input.readInt(), data);
			break;
		case TYPE_ARRAY:
			inputReadArray(array, map, key, input, data);
			break;
		case TYPE_MAP:
			inputReadMap(array, map, key, input, data);
			break;
		case TYPE_KEY_STRING:
			readField(array, map, input.readUTF(), input, data);
			break;
		case TYPE_KEY_INT:
			readField(array, map, input.readInt(), input, data);
			break;
		case TYPE_STACK_RETURN:
			break;
		}
		return type;
	}

	private void inputReadMap(Array array, Map map, Object key, DataInputStream input, SaveData data)
			throws IOException {
		debugField("Map", key);
		Map value = null;
		if (key == null) {
			if (array == null) {
				exception("inputReadMap error! none key array");
			} else {
				value = new HashMap();
				array.add(value);
			}
		} else {
			if (map != null) {
				value = new HashMap();
				map.put(key, value);
			} else {
				value = data.map(key.toString());
			}
		}

		stackLevel++;
		while (readField(null, value, null, input, data) != TYPE_STACK_RETURN) {
		}
		stackLevel--;
	}

	private void inputReadArray(Array array, Map map, Object key, DataInputStream input, SaveData data)
			throws IOException {
		debugField("Array", key);
		Array value = null;
		if (key == null) {
			if (array == null) {
				exception("inputReadArray error! none key array!");
			} else {
				value = new Array();
				array.add(value);
			}
		} else {
			if (map != null) {
				value = new Array();
				map.put(key, value);
			} else {
				value = data.array(key.toString());
			}
		}

		stackLevel++;
		while (readField(value, null, null, input, data) != TYPE_STACK_RETURN) {
		}
		stackLevel--;
	}

	private void inputReadMaskInt(Array array, Map map, Object key, int val, SaveData data) {
		debugField("MaskInt", val);
		MaskInt value = new MaskInt(val);
		if (key == null) {
			if (array == null) {
				exception("ReadError!  readDouble -> key is null!");
			} else {
				array.add(value);
			}
		} else {
			if (map != null) {
				map.put(key, value);
			} else {
				data.save(key.toString(), value);
			}
			key = null;
		}
	}

	private void inputReadDouble(Array array, Map map, Object key, double value, SaveData data) {
		debugField("Double", value);
		if (key == null) {
			if (array == null) {
				exception("ReadError!  readDouble -> key is null!");
			} else {
				array.add(value);
			}
		} else {
			if (map != null) {
				map.put(key, value);
			} else {
				data.save(key.toString(), value);
			}
			key = null;
		}
	}

	private void inputReadFloat(Array array, Map map, Object key, float value, SaveData data) {
		debugField("Float", value);
		if (key == null) {
			if (array == null) {
				exception("ReadError!  readFloat -> key is null!");
			} else {
				array.add(value);
			}
		} else {
			if (map != null) {
				map.put(key, value);
			} else {
				data.save(key.toString(), value);
			}
			key = null;
		}
	}

	private void inputReadLong(Array array, Map map, Object key, long value, SaveData data) {
		debugField("Long", value);
		if (key == null) {
			if (array == null) {
				exception("ReadError!  readLong -> key is null!");
			} else {
				array.add(value);
			}
		} else {
			if (map != null) {
				map.put(key, value);
			} else {
				data.save(key.toString(), value);
			}
			key = null;
		}
	}

	private void inputReadString(Array array, Map map, Object key, String value, SaveData data) {
		debugField("String", value);
		if (key == null) {
			if (array == null) {
				exception("ReadError!  readString -> key is null!");
			} else {
				array.add(value);
			}
		} else {
			if (map != null) {
				map.put(key, value);
			} else {
				data.save(key.toString(), value);
			}
			key = null;
		}
	}

	private void inputReadInt(Array array, Map map, Object key, int value, SaveData data) {
		debugField("int", value);
		if (key == null) {
			if (array == null) {
				exception("ReadError!  readInt -> key is null!");
			} else {
				array.add(value);
			}
		} else {
			if (map != null) {
				map.put(key, value);
			} else {
				data.save(key.toString(), value);
			}
			key = null;
		}
	}

	private void inputReadShort(Array array, Map map, Object key, short value, SaveData data) {
		debugField("short", value);
		if (key == null) {
			if (array == null) {
				exception("ReadError!  readShort -> key is null!");
			} else {
				array.add(value);
			}
		} else {
			if (map != null) {
				map.put(key, value);
			} else {
				data.save(key.toString(), value);
			}
			key = null;
		}
	}

	private void inputReadByte(Array array, Map map, Object key, byte value, SaveData data) {
		debugField("byte", value);
		if (key == null) {
			if (array == null) {
				exception("ReadError!  readByte -> key is null!");
			} else {
				array.add(value);
			}
		} else {
			if (map != null) {
				map.put(key, value);
			} else {
				data.save(key.toString(), value);
			}
			key = null;
		}
	}

	private void inputReadBoolean(Array array, Map map, Object key, boolean value, SaveData data) {
		debugField("boolean", value);
		if (key == null) {
			if (array == null) {
				exception("ReadError!  readBoolean -> key is null!");
			} else {
				array.add(value);
			}
		} else {
			if (map != null) {
				map.put(key, value);
			} else {
				data.save(key.toString(), value);
			}
			key = null;
		}
	}

	public void debug(String msg) {
		if (isLogOut) {
			GM.logger.log(TAG, msg);
		}
	}

	public void debugField(String type, Object value) {
		if (isLogOut) {
			GM.logger.log(TAG, String.format("%-" + (stackLevel + 1) * 4 + "s >readType[%s][%s]", " ", type, value));
		}
	}

	public void log(String msg) {
		GM.logger.error("DefaultSaveDataOperate>", msg);
	}

	public void exception(String msg) {
		throw new RuntimeException(msg);
	}

	@Override
	public void save(SaveData data, OutputStream outStream) {
		if (data == null) {
			data = new SaveData();
		}
		DataOutputStream out = null;
		try {
			out = new DataOutputStream(outStream);

			out.writeInt(FILE_SIGN);

			out.writeInt(TYPE_IS_MUSIC_ON);
			out.writeBoolean(data.isMusicOn);
			out.writeInt(TYPE_IS_SOUND_ON);
			out.writeBoolean(data.isSoundOn);

			for (Map map : data.pools()) {
				for (Object obj : map.entrySet()) {
					Entry e = (Entry) obj;
					writeTypeField(out, e.getKey(), e.getValue());
				}
			}

			out.writeInt(TYPE_STACK_RETURN);

		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (out != null) {
				try {
					out.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

	private void writeTypeField(DataOutputStream out, Object key, Object value) throws IOException {
		if (key instanceof String) {
			out.writeInt(TYPE_KEY_STRING);
			out.writeUTF(key.toString());
		} else if (key instanceof Integer) {
			out.writeInt(TYPE_KEY_INT);
			out.writeInt((Integer) key);
		}
		writeTypeFieldValue(out, value);
	}

	private void writeTypeFieldValue(DataOutputStream out, Object value) throws IOException {
		if (value instanceof Boolean) {
			out.writeInt(TYPE_BOOL);
			out.writeBoolean(((Boolean) value).booleanValue());
		} else if (value instanceof MaskInt) {
			out.writeInt(TYPE_MASKINT);
			out.writeInt(((MaskInt) value).value());
		} else if (value instanceof Number) {
			if (value instanceof Byte) {
				out.writeInt(TYPE_BYTE);
				out.writeByte(((Number) value).byteValue());
			} else if (value instanceof Short) {
				out.writeInt(TYPE_SHORT);
				out.writeShort(((Number) value).shortValue());
			} else if (value instanceof Integer) {
				out.writeInt(TYPE_INT);
				out.writeInt(((Number) value).intValue());
			} else if (value instanceof Long) {
				out.writeInt(TYPE_LONG);
				out.writeLong(((Number) value).longValue());
			} else if (value instanceof Float) {
				out.writeInt(TYPE_FLOAT);
				out.writeFloat(((Number) value).floatValue());
			} else if (value instanceof Double) {
				out.writeInt(TYPE_DOUBLE);
				out.writeDouble(((Number) value).doubleValue());
			}
		} else if (value instanceof Array) {
			out.writeInt(TYPE_ARRAY);
			writeTypeArray(out, (Array) value);
		} else if (value instanceof Map) {
			out.writeInt(TYPE_MAP);
			writeTypeMap(out, (Map) value);
		} else {
			out.writeInt(TYPE_STRING);
			out.writeUTF(value.toString());
		}
	}

	private void writeTypeMap(DataOutputStream out, Map value) throws IOException {
		for (Object obj : value.entrySet()) {
			Entry e = (Entry) obj;
			writeTypeField(out, e.getKey(), e.getValue());
		}
		out.writeInt(TYPE_STACK_RETURN);
	}

	private void writeTypeArray(DataOutputStream out, Array value) throws IOException {
		for (int i = 0; i < value.size; i++) {
			writeTypeFieldValue(out, value.get(i));
		}
		out.writeInt(TYPE_STACK_RETURN);
	}

	///////////////////////

	public static void main(String[] args) {
		isLogOut = true;
		GM.logger = new Logger() {
			@Override
			public void log(String tag, String message, Throwable exception) {
			}

			@Override
			public void log(String tag, String message) {
				System.out.println(message);
			}

			@Override
			public void error(String tag, String message, Throwable exception) {
			}

			@Override
			public void error(String tag, String message) {
				System.out.println(message);
			}

			@Override
			public void debug(String tag, String message, Throwable exception) {
			}

			@Override
			public void debug(String tag, String message) {
				System.out.println(message);
			}
		};

		SaveData data = new SaveData();
		data.isMusicOn = true;
		data.isSoundOn = false;

		data.map("levelStars").put(1, 3);
		data.map("levelStars").put(2, 3);

		// data.save("key1", "STRING value of stringPool.");
		// data.save("isDone", true);
		// data.save("key1", new MaskInt(9891929));
		// data.save("key2", "Another string.");
		//
		// Array<Array<String>> array1 = new Array<Array<String>>();
		// array1.add(new Array<String>(new String[] { "H", "e", "l", "l", "o"
		// }));
		// array1.add(new Array<String>(new String[] { "I'm fine.", "Thanks."
		// }));
		// data.array("array1").add(array1);
		//
		// Array<Array<Map<Integer, Array<Map<String, String>>>>> aamam = new
		// Array<Array<Map<Integer, Array<Map<String, String>>>>>();
		// Array<Map<Integer, Array<Map<String, String>>>> amam = new
		// Array<Map<Integer, Array<Map<String, String>>>>();
		// Map<Integer, Array<Map<String, String>>> mam = new HashMap<Integer,
		// Array<Map<String, String>>>();
		// Array<Map<String, String>> am = new Array<Map<String, String>>();
		// Map<String, String> m = new HashMap<String, String>();
		// m.put("KeY", "VaLuE");
		// am.add(m);
		// mam.put(1, am);
		// amam.add(mam);
		// aamam.add(amam);
		// data.array("deepArray").add(aamam);

		DefaultSaveDataOperation dsdo = new DefaultSaveDataOperation();
		try {
			dsdo.save(data, new FileOutputStream("outFileTest.dat"));
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}

		try {
			SaveData od = dsdo.load(new FileInputStream("outFileTest.dat"));
			System.out.println("isMusicOn:" + od.isMusicOn);
			System.out.println("isSoundOn:" + od.isSoundOn);

			System.out.println(od.map("levelStars"));

			// System.out.println("KEY1:" + od.string("key1"));
			// System.out.println("MaskInt:" + od.maskInt("key1").value());
			//
			// System.out.println(od.arrayPools);
			// System.out.println(od.array("deepArray"));
			// Array<Array<Array<Map<Integer, Array<Map<String, String>>>>>>
			// aamamOUT = od.array("deepArray");
			//
			// String kv = aamamOUT.first()//
			// .first()//
			// .first()//
			// .get(1)//
			// .first()//
			// .get("KeY");
			// System.out.println("KeY:" + kv);

		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}

	}
}
